package com.jgp.common.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.jgp.common.annotation.FtlDirective;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.Column;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.io.*;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Created by Gavin Lou on 16/3/11.
 */
public class JGPUtil {
    
    public static final String DATE_TIME_FORMATTER_PATTERN = "yyyy-MM-dd HH:mm:ss";
    public static final String DATE_FORMATTER_PATTERN = "yyyy-MM-dd";
    public static final String TIME_FORMATTER_PATTERN = "HH:mm:ss";
    public static final SimpleDateFormat S_DATE_TIME_FORMATTER = new SimpleDateFormat(DATE_TIME_FORMATTER_PATTERN);
    public static final SimpleDateFormat S_DATE_FORMATTER = new SimpleDateFormat(DATE_FORMATTER_PATTERN);
    public static final SimpleDateFormat S_TIME_FORMATTER = new SimpleDateFormat(TIME_FORMATTER_PATTERN);
    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER_PATTERN);
    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMATTER_PATTERN);
    public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_FORMATTER_PATTERN);
    public final static ObjectMapper MAPPER;
    private final static Logger logger = LoggerFactory.getLogger(JGPUtil.class);
    private static final Random RM = new Random();
    private static String[] chars = new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"
            , "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"
            , "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
    
    static {
        MAPPER = new ObjectMapper();
        MAPPER.setTimeZone(TimeZone.getDefault());
        MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(TIME_FORMATTER));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
        MAPPER.registerModule(javaTimeModule);
        
        SimpleModule simpleModule = new SimpleModule();
        MAPPER.registerModule(simpleModule);
        MAPPER.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
        MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
        MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    }
    
    public static String decodeBase64(String str) {
        return new String(Base64.getDecoder().decode(str));
    }
    
    public static String encodeBase64(String str) {
        return Base64.getEncoder().encodeToString(str.getBytes()).replaceAll("\n", "");
    }
    
    public static String uuid() {
        return UUID.randomUUID().toString().trim().replaceAll("-", "");
    }
    
    public static String shortUuid(String origin) {
        StringBuffer shortBuffer = new StringBuffer();
        for (int i = 0; i < 8; i++) {
            String str = origin.substring(i * 4, i * 4 + 4);
            int x = Integer.parseInt(str, 16);
            shortBuffer.append(chars[x % 0x3E]);
        }
        return shortBuffer.toString();
    }
    
    /**
     * 获取随机字符串 时间与6位随机数组合
     *
     * @return 随机数
     */
    public static String random() {
        return System.currentTimeMillis() + "" + random(6);
    }
    
    /**
     * 生成指定范围内的随机数
     *
     * @param from 开始数 包含
     * @param to   结束数 包含
     * @return 随机数
     */
    public static int random(int from, int to) {
        return (new Random().nextInt((to - from + 1)) + from);
    }
    
    /**
     * 获取指定长度的随机数字符串
     *
     * @param length 长度
     * @return 随机数
     */
    public static String random(int length) {
        return String.valueOf(RM.nextDouble() * Math.pow(10, length)).replace(".", "").substring(0, length);
    }
    
    /**
     * 获取指定长度的随机字符串
     *
     * @param length 长度
     * @return 随机数
     */
    public static String randomStr(int length) {
        //定义一个字符串（A-Z，a-z，0-9，!@#$%^&*()_+~-=?/|） 即80位；
        String str = "zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890!@#$%^&*()_+~-=?/|";
        //由Random生成随机数
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        //长度为几就循环几次
        for (int i = 0; i < length; ++i) {
            //产生0-79的数字
            int number = random.nextInt(80);
            //将产生的数字通过length次承载到sb中
            sb.append(str.charAt(number));
        }
        //将承载的字符转换成字符串
        return sb.toString();
    }
    
    /**
     * 判断密码一致性
     *
     * @param pass   用户输入的密码
     * @param hashed 加密过的密码
     * @param salt   盐
     * @return true一致 false不一致
     */
    /*public static boolean md5Check(String pass, String hashed, String salt) {
        Md5PasswordEncoder passwordEncoder = new Md5PasswordEncoder();
        passwordEncoder.setEncodeHashAsBase64(true);
        return passwordEncoder.isPasswordValid(hashed, pass, salt);
    }*/
    
    
    /**
     * 对密码进行MD5加密
     *
     * @param pass 密码原始字符串
     * @param salt 盐
     * @return 加密后的字符串
     */
    /*public static String md5(String pass, Object salt) {
        Md5PasswordEncoder passwordEncoder = new Md5PasswordEncoder();
        passwordEncoder.setEncodeHashAsBase64(true);
        return passwordEncoder.encodePassword(pass, salt);
    }*/
    
    
    /**
     * 判断密码一致性
     *
     * @param pass   用户输入的密码
     * @param hashed 加密过的密码
     * @return true一致 false不一致
     */
   /* public static boolean bCryptCheck(String pass, String hashed) {
        return BCrypt.checkpw(pass, hashed);
    }
*/
    /**
     * 对密码进行加密
     *
     * @param pass 密码原始字符串
     * @return 加密后的字符串
     */
   /* public static String bCrypt(String pass) {
        return BCrypt.hashpw(pass, BCrypt.gensalt());
    }
*/
    /**
     * 获取当前用户
     * 启用spring security 才能获取
     *
     * @return
     */
   /* public static Object currentUser() {
        return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }*/
    
    /**
     * 对象转json字符串
     *
     * @param bean 对象
     * @return json字符串
     */
    public static String toJson(Object bean) {
        try {
            return MAPPER.writeValueAsString(bean);
        } catch (JsonProcessingException e) {
            logger.error(bean.toString() + ":转json字符串失败", e);
        }
        return null;
    }
    
    /**
     * 格式json字符串为指定对象
     *
     * @param json  json字符串
     * @param clazz 指定对象类型的CLASS
     * @param <T>   指定对象类型
     * @return 对象
     */
    public static <T> T toBean(String json, Class<T> clazz) {
        try {
            return MAPPER.readValue(json, clazz);
        } catch (IOException e) {
            logger.error("json格式化成对象【" + clazz.getTypeName() + "】失败\n原始JSON:\n" + json, e);
        }
        return null;
    }
    
    /**
     * json 转 List<T>
     *
     * @param json
     * @param clazz T 的 class 类型
     * @param <T>
     * @return
     */
    public static <T> List<T> toListBean(String json, Class<T> clazz) {
        try {
            TypeFactory typeFactory = MAPPER.getTypeFactory();
            JavaType javaType = typeFactory.constructParametricType(ArrayList.class, clazz);
            return MAPPER.readValue(json, javaType);
        } catch (IOException e) {
            logger.error("json格式化成对象【" + clazz.getTypeName() + "】失败\n原始JSON:\n" + json, e);
        }
        return null;
    }
    
    /**
     * 对象装换为map
     *
     * @param bean 对象
     * @param <T>  对象类型泛型
     * @return Map
     */
    /*public static <T> Map<String, Object> toMap(T bean) {
        try {
            return jsonToMap(toJson(bean));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }*/
    public static <T> Map<String, Object> toMap(T bean) {
        return jsonToMap(toJson(bean));
    }
    
    /**
     * 对象装换为map 过滤掉了实体中 被@Transient注解的透明字段
     * 只包含被 @Column修饰的字段
     *
     * @param bean 目标类型 {@link Class}
     * @return Map
     */
    public static Map<String, Object> toDbMap(Object bean) {
        Map<String, Object> map = null;
        if (Objects.nonNull(bean)) {
            map = new HashMap<>();
            Map<String, Object> beanMap = toMap(bean);
            for (Object key : beanMap.keySet()) {
                if (!ReflectUtil.checkFieldAnnotation(bean.getClass(), key.toString(), Transient.class) && ReflectUtil
                        .checkFieldAnnotation(bean.getClass(), key.toString(), Column.class)) {
                    map.put(key + "", beanMap.get(key));
                }
            }
        }
        return map;
    }
    
    /**
     * Map转成对象
     *
     * @param map  map
     * @param type 实体对象
     * @param <T>  实体类型泛型
     * @return 实体
     */
    public static <T> T toBean(Map<String, Object> map, Class<T> type) {
        return toBean(toJson(map), type);
    }
    
    /**
     * json 字符串转 MAP
     *
     * @param json 标准json字符串
     * @return MAP
     * @throws IOException
     */
    public static Map<String, Object> jsonToMap(String json) {
        Map<String, Object> map = null;
        try {
            map = MAPPER.readValue(json, Map.class);
        } catch (IOException e) {
            logger.error("JSON 转 MAP 失败:\n" + json + "\n", e);
        }
        return map;
    }
    
    /**
     * json 字符串转 LIST
     *
     * @param json 标准json字符串
     * @return LIST
     * @throws IOException
     */
    public static <T> List<T> jsonToList(String json, Class<? extends T> type) throws IOException {
        JavaType javaType = MAPPER.getTypeFactory().constructParametricType(ArrayList.class, type);
        return MAPPER.readValue(json, javaType);
    }
    
    public static <T> T json2Bean(String json, Class<? extends T> type) throws IOException {
        JavaType javaType = MAPPER.getTypeFactory().constructType(type);
        return MAPPER.readValue(json, javaType);
    }
    
    
    /**
     * 序列化对象为字符串
     *
     * @param obj 被序列化的对象
     * @return 序列号后的字符串
     */
    public static String serializeToString(Object obj) {
        try (
                ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(byteOut)
        ) {
            objOut.writeObject(obj);
            String str = byteOut.toString("ISO-8859-1");
            return str;
        } catch (IOException ex) {
            return null;
        }
    }
    
    /**
     * 反序列号字符串为对象
     *
     * @param str 字符串
     * @param <O> 对象类型泛型
     * @return 对象
     */
    public static <O> O deserializeToObject(String str) {
        try (
                ByteArrayInputStream byteIn = new ByteArrayInputStream(str.getBytes("ISO-8859-1"));
                ObjectInputStream objIn = new ObjectInputStream(byteIn)
        ) {
            return (O) objIn.readObject();
        } catch (IOException | ClassNotFoundException e) {
            return null;
        }
    }
    
    /**
     * 正则抽取字符串内容
     *
     * @param source 原始字符串
     * @param regex  正则表达式
     * @return 返回符合正则抽取条件的字符串集合
     */
    public static List<String> regexExtract(String source, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(source);
        List<String> list = new ArrayList<>();
        while (matcher.find()) {
            list.add(matcher.group());
        }
        return list;
    }
    
    /**
     * 正则抽取字符串内容
     *
     * @param source 原始字符串
     * @param regex  正则表达式
     * @return 返回符合正则抽取条件的字符串集合
     */
    public static String regexExtractOne(String source, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(source);
        List<String> list = new ArrayList<>();
        while (matcher.find()) {
            list.add(matcher.group());
        }
        return list.size() > 0 ? list.get(0) : null;
    }
    
    public static boolean isContainChinese(String str) {
        
        Pattern p = Pattern.compile("[\u4e00-\u9fa5]");
        Matcher m = p.matcher(str);
        return m.find();
    }
    
    /**
     * 替换模板变量
     *
     * @param tpl  "xxx{name}eweweqe{age}0wowk"
     * @param data {'张三':'','age':'15'}
     * @return "xxx张三eweweqe150wowk"
     */
    public static String template(String tpl, Map<String, String> data) {
        for (String key : data.keySet()) {
            tpl = tpl.replaceAll("\\$\\{".concat(key).concat("\\}"), data.get(key));
        }
        return tpl;
    }
    
    /**
     * 合并对象
     * 传入要合并的对象，排序越大属性保留的可能性越大，以第一个对象的属性种类为准，
     * 后面的对象如果此属性不为null则可覆盖前面对象的该属性
     *
     * @param beans 实体
     * @param <O>   此方法为泛型方法
     * @return 合并后对象
     */
    public static <O> O merge(O... beans) {
        if (Objects.isNull(beans)) return null;
        Map<String, Object> merged = Arrays.asList(beans).stream().map(JGPUtil::toMap).map(Map::entrySet).flatMap(Set::stream)
                                           .filter(e -> e.getValue() != null).collect(HashMap::new, (m, v) -> {
                    m.put(v.getKey(), v.getValue());
                }, HashMap::putAll);
        
        
        return toBean(merged, (Class<O>) beans[0].getClass());
    }
    
    /**
     * 合并对象 将MAP中的值合并到对象中
     * 可传入多个map，排序越大属性保留的可能性越大，以对象的属性种类为准
     * map排序越大 其属性 越有覆盖权
     *
     * @param target 目标对象
     * @param maps   map
     * @param <O>    此方法为泛型方法
     * @return 合并后对象
     */
    public static <O> O merge(O target, Map<String, Object>... maps) {
        if (Objects.isNull(target) || Objects.isNull(maps)) return null;
        Map<String, Object> originalMap = toMap(target);
        Map<String, Object> merged = Arrays.asList(maps).stream().filter(Objects::nonNull).map(Map::entrySet).flatMap(Set::stream)
                                           .filter(e -> originalMap.containsKey(e.getKey())).collect(HashMap::new, (m, v) -> {
                    m.put(v.getKey(), v.getValue());
                }, HashMap::putAll);
        originalMap.putAll(merged);
        return toBean(originalMap, (Class<O>) target.getClass());
    }
    
    /**
     * 下划线转驼峰法
     *
     * @param line       源字符串
     * @param smallCamel 大小驼峰,是否为小驼峰 true 为小写驼峰 accAcc
     * @return 转换后的字符串
     */
    public static String underline2Camel(String line, boolean smallCamel) {
        if (line == null || "".equals(line)) {
            return "";
        }
        StringBuffer sb = new StringBuffer();
        Pattern pattern = Pattern.compile("([A-Za-z\\d]+)(_)?");
        Matcher matcher = pattern.matcher(line);
        while (matcher.find()) {
            String word = matcher.group();
            sb.append(smallCamel && matcher.start() == 0 ? Character.toLowerCase(word.charAt(0)) : Character.toUpperCase(word.charAt(0)));
            int index = word.lastIndexOf('_');
            if (index > 0) {
                sb.append(word.substring(1, index).toLowerCase());
            } else {
                sb.append(word.substring(1).toLowerCase());
            }
        }
        return sb.toString();
    }
    
    /**
     * 驼峰法转下划线
     *
     * @param line 源字符串
     * @return 转换后的字符串
     */
    public static String camel2Underline(String line) {
        if (line == null || "".equals(line)) {
            return "";
        }
        line = String.valueOf(line.charAt(0)).toUpperCase().concat(line.substring(1));
        StringBuffer sb = new StringBuffer();
        Pattern pattern = Pattern.compile("[A-Z]([a-z\\d]+)?");
        Matcher matcher = pattern.matcher(line);
        while (matcher.find()) {
            String word = matcher.group();
            sb.append(word.toUpperCase());
            sb.append(matcher.end() == line.length() ? "" : "_");
        }
        return sb.toString();
    }
    
    public static Map freemarkerSharedVaribles() {
        Map varibles = new HashMap();
        String[] pkgs = new String[]{"com.jgp.ftl.**"};
        Set<Class<?>> classes = ScannerUtil.scanByAnnotation(new Class[]{FtlDirective.class},pkgs);
        classes.forEach(clazz -> {
            FtlDirective directive = clazz.getAnnotation(FtlDirective.class);
            String name = directive.value();
            try {
                varibles.put(name,clazz.newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        });
        return varibles;
    }
    
    /**
     * LocalDate 转 Date
     *
     * @param localDate
     * @return
     */
    public static Date localDateToDate(LocalDate localDate) {
        ZoneId zone = ZoneId.systemDefault();
        Instant instant = localDate.atStartOfDay().atZone(zone).toInstant();
        return Date.from(instant);
    }
    
    /**
     * LocalDateTime 转 Date
     *
     * @param localDateTime
     * @return
     */
    public static Date localDateTimeToDate(LocalDateTime localDateTime) {
        ZoneId zone = ZoneId.systemDefault();
        Instant instant = localDateTime.atZone(zone).toInstant();
        return Date.from(instant);
    }
    
    /**
     * LocalDateTime 转 Long
     *
     * @param localDateTime
     * @return
     */
    public static Long localDateTimeToLong(LocalDateTime localDateTime) {
        ZoneId zone = ZoneId.systemDefault();
        Instant instant = localDateTime.atZone(zone).toInstant();
        return instant.toEpochMilli();
    }
    
    /**
     * LocalTime 转 Date
     *
     * @param localTime
     * @return
     */
    public static Date localTimeToDate(LocalTime localTime) {
        LocalDate localDate = LocalDate.now();
        LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
        ZoneId zone = ZoneId.systemDefault();
        Instant instant = localDateTime.atZone(zone).toInstant();
        return Date.from(instant);
    }
    
    /**
     * 小写第一个字母
     *
     * @param str 字符串
     * @return 字符串
     */
    public static String lowerFirstCase(String str) {
        return str.substring(0, 1).toLowerCase() + str.substring(1);
    }
    
    /**
     * 大写第一个字母
     *
     * @param str 字符串
     * @return 字符串
     */
    public static String upperFirstCase(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }
    
    /**
     * 判断目标时间到当前时间是否超过规定范围
     *
     * @param targetTime 目标时间
     * @param seconds    范围，秒为单位
     * @return true 是，false 否
     */
    public static boolean isTimeOut(LocalDateTime targetTime, long seconds) {
        LocalDateTime now = LocalDateTime.now();
        return targetTime.plusSeconds(seconds).isBefore(now);
    }
    
    /**
     * 获取某天的开始时间
     *
     * @param date 目标日期
     * @return 开始时间
     */
    public static LocalDateTime startOfDate(LocalDate date) {
        return LocalDateTime.of(date, LocalTime.MIN);
    }
    
    /**
     * 获取某天的结束时间
     *
     * @param date 目标日期
     * @return 结束时间
     */
    public static LocalDateTime endOfDate(LocalDate date) {
        return LocalDateTime.of(date, LocalTime.MAX);
    }
    
    public static LocalDate startOfMonth(LocalDate date){
        return LocalDate.of(date.getYear(),date.getMonth(),1);
    }
    
    public static LocalDate endOfMonth(LocalDate date){
        Month month = date.getMonth();
        return LocalDate.of(date.getYear(),month,month.length(Year.isLeap(date.getYear())));
    }
    
    public static LocalDateTime dateToLocalDateTime(Date date) {
        return Objects.isNull(date) ? null : LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
    }
    
    public static LocalDate dateToLocalDate(Date date) {
        return Objects.isNull(date) ? null : dateToLocalDateTime(date).toLocalDate();
    }
    
    public static LocalTime dateToLocalTime(Date date) {
        return Objects.isNull(date) ? null : dateToLocalDateTime(date).toLocalTime();
    }
    
    
    public static Map<String, Object> castToRequestMapMultiObject(Object model) {
        String prefix = getModelPrefix(model.getClass());
        Map<String, Object> map = JGPUtil.toMap(model);
        Map<String, Object> result = new HashMap<>();
        map.forEach((s, o) -> {
            if (Objects.isNull(o)) return;
            result.put(prefix + "." + s, o + "");
        });
        return result;
    }
    
    public static Map<String, Object> castToRequestMap(Object model) {
        Map<String, Object> map = JGPUtil.toMap(model);
        Map<String, Object> result = new HashMap<>();
        map.forEach((s, o) -> {
            if (Objects.isNull(o)) return;
            result.put(s, o + "");
        });
        return result;
    }
    
    public static String getModelPrefix(Class clazz) {
        String name = clazz.getName();
        name = name.substring(name.lastIndexOf(".") + 1);
        return Character.toLowerCase(name.charAt(0)) + name.substring(1);
    }
    
    
    public static <T> List<T> intersection(List<T> one, List<T> two) {
        return one.stream().filter(t -> two.contains(t)).collect(Collectors.toList());
    }
    
    public static <T> List<T> difference(List<T> one, List<T> two) {
        return one.stream().filter(t -> !two.contains(t)).collect(Collectors.toList());
    }
    
    public static <T> List<T> union(List<T> one, List<T> two) {
        List<T> list = new ArrayList<>();
        list.addAll(one);
        list.addAll(two);
        return list.stream().distinct().collect(Collectors.toList());
    }
    
    public static <T> InsertUpdateDelete<T> iud(List<T> newItems, List<T> dbItems) {
        List<T> updates = intersection(newItems, dbItems);
        List<T> deletes = difference(dbItems, newItems);
        List<T> inserts = difference(newItems, updates);
        return new InsertUpdateDelete<>(inserts, updates, deletes);
    }
    
    public static String tableName(Class<?> clazz) {
        Table table = clazz.getAnnotation(Table.class);
        String name = null;
        if (Objects.nonNull(table)) {
            name = table.name();
        }
        return name;
    }
    
}